<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>数独题目生成器</title>
<style>
body{font-family:'Microsoft YaHei',sans-serif;max-width:600px;margin:0 auto;padding:2px;background-color:#f5f5f5;}
 h1{color:#333;text-align:center;}
 .panel{background-color:white;border-radius:8px;padding:20px;box-shadow:0 2px 5px rgba(0,0,0,0.1);}
 .controls{display:flex;gap:5px;margin-bottom:5px;}
 .grp{ flex:1;padding:3px;}
 label{display:block;margin-bottom:5px;font-weight:bold;}
 input,button{width:100%;padding:8px;margin-bottom:10px;box-sizing:border-box;}
 button{background-color:#4CAF50;color:white;border:none;cursor:pointer;font-weight:bold;transition:background-color 0.3s;}
 button:hover{background-color:#45a049;}
 button:disabled{background-color:#cccccc;cursor:not-allowed;}
 .sudoku-container{display:flex;gap:0px;justify-content:space-between;margin-top:0px;}
 .sudoku-grid{display:grid;grid-template-columns:repeat(9,1fr);gap:0px;border:2px solid #333;width:100%;aspect-ratio:1;}
 .cell{display:flex;align-items:center;justify-content:center;background-color:white;min-width:20px;min-height:20px;font-size:18px;font-weight:bold;border:1px solid #ccc;aspect-ratio:1;position:relative;}
 .cell:nth-child(3n){border-right:2px solid #333;}
 .cell:nth-child(9n){border-right:none;}
 .cell:nth-child(n+19):nth-child(-n+27),.cell:nth-child(n+46):nth-child(-n+54){border-bottom:2px solid #333;}
 .cell span:empty{display:inline-block;width:100%;height:100%;}
 .cell span:not(:empty){display:flex;align-items:center;justify-content:center;width:100%;height:100%;}
 .cell:nth-child(n+19):nth-child(-n+27),.cell:nth-child(n+46):nth-child(-n+54){margin-bottom:1px;}
 .status{margin-top:15px;padding:10px;border-radius:4px;background-color:#f8f8f8;font-size:14px;}
 .success{color:#4CAF50;}
 .error{color:#f44336;}
 .info{color:#2196F3;}
 .export-area{margin-top:20px;}
 textarea{width:100%;min-height:120px;margin-top:10px;resize:vertical;} 
</style>
</head>
<body>
<h1>数独题目生成器</h1>
<div class="panel">
<div class="controls">
<div class="grp">
<label for="timeout">超时时间(毫秒):</label>
<input type="number" id="timeout" value="3000" min="1000">
</div>
<div class="grp">
<label for="maxDepth">最大尝试深度:</label>
<input type="number" id="maxDepth" value="5" min="1" max="10">
</div>
<div class="grp">
<label for="targetHoles">目标空格数:</label>
<input type="number" id="targetHoles" value="63" min="30" max="64">
</div>
</div>
<div class="controls">
<button id="generateBtn">生成数独题目</button>
<button id="stopBtn" disabled>停止生成</button>
</div>  
<div class="status" id="generateStatus"></div>
<div class="sudoku-container">
<div class="grp">
<h3>题目</h3>
<div class="sudoku-grid" id="puzzleGrid"></div>
</div>
<div class="grp">
<h3>答案</h3>
<div class="sudoku-grid" id="solutionGrid"></div>
</div>
</div>
<div class="export-area">
<!--button id="exportBtn">导出当前题目</button-->
<textarea id="exportOutput" placeholder="导出的题目将显示在这里..."></textarea>
</div>
</div>
<script>
// 全局变量
let isGenerating = false;
let stopGeneration = false;
let currentPuzzle = null;
let currentSolution = null;
// 初始化页面
document.addEventListener('DOMContentLoaded', function() {
// 初始化数独网格
initGrid('puzzleGrid');
initGrid('solutionGrid');
// 绑定按钮事件
document.getElementById('generateBtn').addEventListener('click', generateSudoku);
document.getElementById('stopBtn').addEventListener('click', function() {
stopGeneration = true;
});
document.getElementById('exportBtn').addEventListener('click', exportCurrentPuzzle);
});
// 初始化数独网格
function initGrid(gridId) {
const grid = document.getElementById(gridId);
grid.innerHTML = '';
for (let i = 0; i < 81; i++) {
const cell = document.createElement('div');
cell.className = 'cell';
const span = document.createElement('span');
span.textContent = '';
cell.appendChild(span);
grid.appendChild(cell);
}
}
// 生成数独题目
async function generateSudoku() {
const timeout = parseInt(document.getElementById('timeout').value);
const maxDepth = parseInt(document.getElementById('maxDepth').value);
const targetHoles = parseInt(document.getElementById('targetHoles').value);
if (targetHoles < 17 || targetHoles > 64) {
updateStatus('generateStatus', '目标空格数应在17-64之间', 'error');
return;
}
isGenerating = true;
stopGeneration = false;
document.getElementById('generateBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
updateStatus('generateStatus', '正在生成数独题目...', 'info');
const startTime = performance.now();
let result = null;
try {
result = await generateSudokuWithTimeout(targetHoles, maxDepth, timeout);
} catch (error) {
console.error('生成错误:', error);
updateStatus('generateStatus', '生成过程中出错: ' + error.message, 'error');
}
const endTime = performance.now();
const timeTaken = (endTime - startTime).toFixed(2);
if (stopGeneration) {
updateStatus('generateStatus', '生成已停止', 'error');
} else if (result) {
currentPuzzle = result.puzzle;
currentSolution = result.solution;
displaySudoku('puzzleGrid', currentPuzzle);
displaySudoku('solutionGrid', currentSolution);
  exportCurrentPuzzle();
updateStatus('generateStatus', `生成成功! 空格数: ${result.holes}, 耗时: ${timeTaken}ms`, 'success');
} else {
updateStatus('generateStatus', '生成失败: 超时或无法找到唯一解', 'error');
}
document.getElementById('generateBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
isGenerating = false;
stopGeneration = false;
}
// 带超时的数独生成
function generateSudokuWithTimeout(targetHoles, maxDepth, timeout) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new Error('超时'));
}, timeout);
// 使用setTimeout模拟异步生成过程
setTimeout(() => {
try {
const result = generateSudokuWithUniqueSolution(targetHoles, maxDepth);
clearTimeout(timeoutId);
resolve(result);
} catch (error) {
clearTimeout(timeoutId);
reject(error);
}
}, 0);
});
}
// 生成具有唯一解的数独
function generateSudokuWithUniqueSolution(targetHoles, maxDepth) {
// 生成完整的数独
const completeSudoku = generateCompleteSudoku();
// 尝试创建具有唯一解的题目
let puzzle = JSON.parse(JSON.stringify(completeSudoku));
let holes = 0;
let positions = [];
// 生成所有可能的位置
for (let i = 0; i < 81; i++) {
positions.push(i);
}
// 随机打乱位置
shuffleArray(positions);
// 尝试挖空
let attempts = 0;
const maxAttempts = 81 * 2;
while (holes < targetHoles && attempts < maxAttempts && positions.length > 0) {
attempts++;
const pos = positions.pop();
const row = Math.floor(pos / 9);
const col = pos % 9;
if (puzzle[row][col] === 0) continue;
const temp = puzzle[row][col];
puzzle[row][col] = 0;
// 检查是否有唯一解
const numSolutions = countSolutions(JSON.parse(JSON.stringify(puzzle)));
if (numSolutions === 1) {
holes++;
} else {
// 恢复这个位置
puzzle[row][col] = temp;
}
}
// 如果空格数不够，尝试更激进的方法
if (holes < targetHoles) {
const result = reduceHolesWithUniqueSolution(
completeSudoku,
puzzle,
targetHoles,
maxDepth
);
puzzle = result.puzzle;
holes = countHoles(puzzle);
}
return {
puzzle: puzzle,
solution: completeSudoku,
holes: holes
};
}
// 生成完整的数独
function generateCompleteSudoku() {
// 创建一个空的9x9数独
const sudoku = Array(9).fill().map(() => Array(9).fill(0));
// 填充对角线上的3x3方块
fillDiagonalBoxes(sudoku);
// 解决剩余的数独
solveSudoku(sudoku);
return sudoku;
}
// 填充对角线上的3x3方块
function fillDiagonalBoxes(sudoku) {
for (let box = 0; box < 9; box += 3) {
fillBox(sudoku, box, box);
}
}
// 填充一个3x3方块
function fillBox(sudoku, row, col) {
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9];
shuffleArray(nums);
let index = 0;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
sudoku[row + i][col + j] = nums[index++];
}
}
}
// 打乱数组
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
return array;
}
// 数独求解算法
function solveSudoku(board) {
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (board[row][col] === 0) {
for (let num = 1; num <= 9; num++) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
if (solveSudoku(board)) {
return true;
}
board[row][col] = 0;
}
}
return false;
}
}
}
return true;
}
// 检查数字是否有效
function isValid(board, row, col, num) {
// 检查行
for (let x = 0; x < 9; x++) {
if (board[row][x] === num) return false;
}
// 检查列
for (let x = 0; x < 9; x++) {
if (board[x][col] === num) return false;
}
// 检查3x3方块
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (board[boxRow + i][boxCol + j] === num) return false;
}
}
return true;
}
// 计算解的个数
function countSolutions(board) {
// 复制一份板子
const boardCopy = JSON.parse(JSON.stringify(board));
let count = 0;
countSolutionsHelper(boardCopy, 0, 0);
return count;
function countSolutionsHelper(board, row, col) {
if (row === 9) {
count++;
return;
}
if (col === 9) {
countSolutionsHelper(board, row + 1, 0);
return;
}
if (board[row][col] !== 0) {
countSolutionsHelper(board, row, col + 1);
return;
}
for (let num = 1; num <= 9; num++) {
if (isValid(board, row, col, num)) {
board[row][col] = num;
countSolutionsHelper(board, row, col + 1);
if (count > 1) return; // 提前终止如果已经有多解
board[row][col] = 0;
}
}
}
}
// 减少空格数同时保持唯一解
function reduceHolesWithUniqueSolution(completeSudoku, puzzle, targetHoles, maxDepth) {
let currentPuzzle = JSON.parse(JSON.stringify(puzzle));
let currentHoles = countHoles(currentPuzzle);
if (currentHoles >= targetHoles) {
return { puzzle: currentPuzzle, holes: currentHoles };
}
// 获取所有非空格位置
let fixedPositions = [];
for (let i = 0; i < 81; i++) {
const row = Math.floor(i / 9);
const col = i % 9;
if (currentPuzzle[row][col] !== 0) {
fixedPositions.push({ row, col, value: completeSudoku[row][col] });
}
}
// 随机打乱固定位置
shuffleArray(fixedPositions);
// 尝试减少固定位置
for (let depth = 1; depth <= maxDepth && currentHoles < targetHoles; depth++) {
for (let i = 0; i < fixedPositions.length && currentHoles < targetHoles; i++) {
const pos = fixedPositions[i];
if (currentPuzzle[pos.row][pos.col] === 0) continue;
// 临时去掉这个固定值
const temp = currentPuzzle[pos.row][pos.col];
currentPuzzle[pos.row][pos.col] = 0;
// 检查是否有唯一解
const numSolutions = countSolutions(JSON.parse(JSON.stringify(currentPuzzle)));
if (numSolutions === 1) {
currentHoles++;
fixedPositions.splice(i, 1);
i--;
} else {
// 恢复这个位置
currentPuzzle[pos.row][pos.col] = temp;
}
}
}
return { puzzle: currentPuzzle, holes: currentHoles };
}
// 计算空格数
function countHoles(puzzle) {
let count = 0;
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
if (puzzle[row][col] === 0) count++;
}
}
return count;
}
// 显示数独
function displaySudoku(gridId, sudoku) {
const grid = document.getElementById(gridId);
const cells = grid.querySelectorAll('.cell span');
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
const index = row * 9 + col;
//cells[index].textContent = sudoku[row][col] !== 0 ? sudoku[row][col] : '';
cells[index].textContent = sudoku[row][col] !== 0 ? sudoku[row][col] : '';
}
}
}
// 更新状态显示
function updateStatus(elementId, message, type) {
const element = document.getElementById(elementId);
element.textContent = message;
element.className = 'status ' + type;
}
// 导出当前题目 exportCurrentPuzzle
function exportCurrentPuzzle() {
if (!currentPuzzle || !currentSolution) {
alert('没有当前题目可导出');
return;
}
let output = '题目:\n';
output += formatSudokuForExport(currentPuzzle);
output += '\n答案:\n';
output += formatSudokuForExport(currentSolution);
document.getElementById('exportOutput').value = output + '\n\n\n' + document.getElementById('exportOutput').value;
}
// 格式化数独用于导出
function formatSudokuForExport(sudoku) {
let result = '';
for (let row = 0; row < 9; row++) {
for (let col = 0; col < 9; col++) {
result += sudoku[row][col] !== 0 ? sudoku[row][col] : '0';
result += '';
}
result += '';
}
return result;
}
</script>
</body>
</html>